use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
    fmt::{Display, Formatter},
    str::FromStr,
};
use tonic::transport::Uri;

const NVME_NQN_UUID_PRE: &str = "nqn.2014-08.org.nvmexpress:uuid:";
const NVME_NQN_MIN_LEN: usize = 11;
const NVME_NQN_MAX_LEN: usize = 223;
const DOMAIN_LABEL_MAX_LEN: usize = 63;

/// An NVMe NQN type wrapper which can be converted from a string with validation.
/// There are two supported NQN formats. The first format may be used by any organization that owns
/// a domain name.
/// The second format may be used to create a unique identifier when there is not a naming authority
/// or there is not a requirement for a human interpretable string.
/// For more information please refer to the NVMe SPEC, eg:
/// https://nvmexpress.org/wp-content/uploads/NVMe-NVM-Express-2.0a-2021.07.26-Ratified.pdf
/// Chapter 4.5.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum NvmeNqn {
    /// This naming format 1 may be used to create a human interpretable string to describe
    /// the host or NVM subsystem. This format consists of:
    /// • The string “nqn”;
    /// • The string “.” (i.e., the ASCII period character);
    /// • A date code, in “yyyy-mm” format. This date shall be during a time interval when the
    /// naming authority owned the domain name used in this format. The date code uses the
    /// Gregorian calendar. All digits and the dash shall be included;
    /// • The string “.” (i.e., the ASCII period character);
    /// • The reverse domain name of the naming authority that is creating the NQN; and
    /// • A colon (:) prefixed string that the owner of the domain name assigns that does not
    /// exceed the maximum length. The naming authority is responsible to ensure that the NQN
    /// is worldwide unique.
    /// The reverse domain name in an NQN that uses this format shall not be “org.nvmexpress”.
    /// The following are examples of NVMe Qualified Names that may be generated by “Example NVMe,
    /// Inc.” NVM Express® Base Specification, revision 2.0a
    /// 142
    /// • The string “nqn.2014-08.com.example:nvme:nvm-subsystem-sn-d78432”; and
    /// • The string “nqn.2014-08.com.example:nvme.host.sys.xyz”.
    Org {
        date: String,
        domain: String,
        name: String,
    },
    /// Format 2 consists of:
    /// • The string “nqn”;
    /// • The string “.” (i.e., the ASCII period character);
    /// • The string “2014-08.org.nvmexpress:uuid:”; and
    /// • A 128-bit UUID based on the definition in RFC 4122 represented as a string formatted as
    /// “11111111-2222-3333-4444-555555555555”.
    /// The following is an example of an NVMe Qualified Name using the UUID-based format:
    /// • The string “nqn.2014-08.org.nvmexpress:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6“.
    Unique { uuid: uuid::Uuid },
    /// Invalid Nqn Format, may be useful as a temporary placeholder.
    Invalid { nqn: String },
}
impl Default for NvmeNqn {
    fn default() -> Self {
        let uuid = uuid::Uuid::default();
        Self::Unique { uuid }
    }
}
impl Serialize for NvmeNqn {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}
impl<'de> Deserialize<'de> for NvmeNqn {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Self::try_from(s).map_err(|e| serde::de::Error::custom(format!("{e:?}")))
    }
}

impl Display for NvmeNqn {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let nqn_str = match self {
            NvmeNqn::Unique { uuid } => format!("{NVME_NQN_UUID_PRE}{uuid}"),
            NvmeNqn::Org { date, domain, name } => format!("nqn.{date}.{domain}:{name}"),
            NvmeNqn::Invalid { nqn } => nqn.to_string(),
        };
        f.write_str(&nqn_str)
    }
}

impl NvmeNqn {
    /// Generate new random `Self`.
    pub fn new() -> Self {
        let uuid = uuid::Uuid::new_v4();
        Self::Unique { uuid }
    }
    /// Generate a product type name.
    pub fn from_nodename(name: &String) -> Self {
        Self::Org {
            date: utils::constants::NVME_NQN_DATE.to_string(),
            domain: utils::constants::NVME_NQN_ORG.to_string(),
            name: format!("{}:{name}", utils::constants::NVME_HOST_NQN),
        }
    }
    /// Get the org name, if the nqn is in the Org format.
    pub fn org_name(&self) -> Option<&String> {
        match self {
            NvmeNqn::Org { name, .. } => Some(name),
            NvmeNqn::Unique { .. } => None,
            NvmeNqn::Invalid { .. } => None,
        }
    }
    pub fn nqn_prefix(&self) -> Option<String> {
        match self {
            NvmeNqn::Org { date, domain, .. } => Some(format!("nqn.{date}.{domain}")),
            NvmeNqn::Unique { .. } => {
                Some(NVME_NQN_UUID_PRE[..NVME_NQN_UUID_PRE.len()].to_string())
            }
            NvmeNqn::Invalid { .. } => None,
        }
    }
    fn parse_nqn_date(date: &str) -> Result<String, NvmeNqnParseError> {
        match date.split("").collect::<Vec<_>>()[..] {
            ["", y1, y2, y3, y4, "-", m1, m2, ..] => {
                match (
                    char::from_str(y1),
                    char::from_str(y2),
                    char::from_str(y3),
                    char::from_str(y4),
                    char::from_str(m1),
                    char::from_str(m2),
                ) {
                    (Ok(y1), Ok(y2), Ok(y3), Ok(y4), Ok(m1), Ok(m2))
                        if y1.is_numeric()
                            && y2.is_numeric()
                            && y3.is_numeric()
                            && m1.is_numeric()
                            && m2.is_numeric() =>
                    {
                        Ok(format!("{y1}{y2}{y3}{y4}-{m1}{m2}"))
                    }
                    _ => Err(NvmeNqnParseError::InvalidDate),
                }
            }
            _ => Err(NvmeNqnParseError::InvalidDateFormat),
        }
    }
}

/// Errors encountered when parsing an `NvmeNqn`.
#[derive(Debug, Eq, PartialEq)]
pub enum NvmeNqnParseError {
    /// Nothing to parse.
    Empty,
    /// The NQN is too long.
    TooLong,
    /// No "nqn.` prefix found.
    NoNqnPrefix,
    /// The uuid part of format2 is invalid.
    InvalidUuid,
    /// The date format is invalid.
    InvalidDateFormat,
    /// The date itself is invalid.
    InvalidDate,
    /// The domain name is too long.
    DomainTooLong,
    /// Missing name string.
    NoName,
    /// No domain found.
    NoDomain,
}

impl From<uuid::Uuid> for NvmeNqn {
    fn from(uuid: uuid::Uuid) -> Self {
        Self::Unique { uuid }
    }
}
impl From<NvmeNqn> for String {
    fn from(nqn: NvmeNqn) -> Self {
        nqn.to_string()
    }
}
impl TryFrom<String> for NvmeNqn {
    type Error = NvmeNqnParseError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::try_from(value.as_str())
    }
}
impl TryFrom<&String> for NvmeNqn {
    type Error = NvmeNqnParseError;

    fn try_from(value: &String) -> Result<Self, Self::Error> {
        Self::try_from(value.as_str())
    }
}
impl TryFrom<Uri> for NvmeNqn {
    type Error = NvmeNqnParseError;

    fn try_from(value: Uri) -> Result<Self, Self::Error> {
        let mut path = value.path();
        if path.starts_with('/') {
            path = &path[1..];
        }
        Self::try_from(path)
    }
}
impl TryFrom<&str> for NvmeNqn {
    type Error = NvmeNqnParseError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let len = value.len();
        if len > NVME_NQN_MAX_LEN {
            return Err(NvmeNqnParseError::TooLong);
        }
        if len < NVME_NQN_MIN_LEN {
            return Err(NvmeNqnParseError::Empty);
        }

        // Check for equality with the generic nqn structure of the form
        // "nqn.2014-08.org.nvmexpress:uuid:11111111-2222-3333-4444-555555555555"
        let parts = value.split(NVME_NQN_UUID_PRE).collect::<Vec<_>>();
        if let ["", uuid] = parts[..] {
            return Ok(Self::Unique {
                uuid: uuid::Uuid::parse_str(uuid).map_err(|_| Self::Error::InvalidUuid {})?,
            });
        }

        // If the nqn does not match the uuid structure, the next several checks validate the form
        // "nqn.yyyy-mm.reverse.domain:name-string"
        let ym_next = match value.strip_prefix("nqn.") {
            Some(ym_next) => Ok(ym_next),
            None => Err(NvmeNqnParseError::NoNqnPrefix),
        }?;
        let date = Self::parse_nqn_date(ym_next)?;
        let (domain, name) = match ym_next.strip_prefix(&format!("{date}.")) {
            Some(nqn_next) => {
                let mut domain = "";
                let mut name = "".to_string();
                for (index, value) in nqn_next.split(':').enumerate() {
                    if index == 0 {
                        domain = value;
                    } else {
                        if !name.is_empty() {
                            name.push(':');
                        }
                        name.push_str(value);
                    }
                }
                if domain.len() > DOMAIN_LABEL_MAX_LEN {
                    Err(NvmeNqnParseError::DomainTooLong)
                } else if domain.is_empty() {
                    Err(NvmeNqnParseError::NoDomain)
                } else if name.is_empty() {
                    Err(NvmeNqnParseError::NoName)
                } else {
                    Ok((domain.to_string(), name))
                }
            }
            None => Err(NvmeNqnParseError::InvalidDateFormat),
        }?;

        // # Warning:
        // Domain and name string not validated
        Ok(Self::Org { date, domain, name })
    }
}

#[test]
fn validate_nvme_nqn() {
    let uuid = uuid::Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap();
    let valid = "nqn.2014-08.org.nvmexpress:uuid:11111111-2222-3333-4444-555555555555";
    let nqn = NvmeNqn::try_from(valid);
    assert_eq!(nqn, Ok(NvmeNqn::Unique { uuid }));
    let valid = "nqn.2014-08.borg.nvmexpress:uuid:11122111-2222-3333-4444-555555555555";
    let nqn = NvmeNqn::try_from(valid);
    assert_eq!(
        nqn,
        Ok(NvmeNqn::Org {
            date: "2014-08".to_string(),
            domain: "borg.nvmexpress".to_string(),
            name: "uuid:11122111-2222-3333-4444-555555555555".to_string()
        })
    );
    let valid = "nqn.2011-11.borg:disk1";
    let nqn = NvmeNqn::try_from(valid);
    assert_eq!(
        nqn,
        Ok(NvmeNqn::Org {
            date: "2011-11".to_string(),
            domain: "borg".to_string(),
            name: "disk1".to_string()
        })
    );
    let valid = "nqn.2011-11.com.test:uuid:disk1";
    let nqn = NvmeNqn::try_from(valid);
    assert_eq!(
        nqn,
        Ok(NvmeNqn::Org {
            date: "2011-11".to_string(),
            domain: "com.test".to_string(),
            name: "uuid:disk1".to_string()
        })
    );
    let invalid = "nqn.2014-08.org.nvmexpress:uuid:11111111-2222-3333-4444-5555555555555";
    let nqn = NvmeNqn::try_from(invalid);
    assert_eq!(nqn, Err(NvmeNqnParseError::InvalidUuid));
    let invalid = "nqn.201-11.borg:11111111-2222-3333-4444-555555555555";
    let error = NvmeNqn::try_from(invalid);
    assert_eq!(error, Err(NvmeNqnParseError::InvalidDateFormat));
    let invalid = "nqn.201408.nqn.org.borg:11111111-2222-3333-4444-555555555555";
    let error = NvmeNqn::try_from(invalid);
    assert_eq!(error, Err(NvmeNqnParseError::InvalidDateFormat));
    let invalid = "nqn.2014-a8.nqn.org.borg::11111111-2222-3333-4444-555555555555";
    let error = NvmeNqn::try_from(invalid);
    assert_eq!(error, Err(NvmeNqnParseError::InvalidDate));
    let invalid = "nqn.2014-08.nqn.org.borg:";
    let error = NvmeNqn::try_from(invalid);
    assert_eq!(error, Err(NvmeNqnParseError::NoName));
    let invalid = "nqn.2014-08.:a";
    let error = NvmeNqn::try_from(invalid);
    assert_eq!(error, Err(NvmeNqnParseError::NoDomain));
    let invalid = "";
    let error = NvmeNqn::try_from(invalid);
    assert_eq!(error, Err(NvmeNqnParseError::Empty));
    let base_valid = "nqn.2014-08.org.borg.long:";
    let long_name = "a".repeat(NVME_NQN_MAX_LEN - base_valid.len());
    let valid = format!("{base_valid}{long_name}");
    let nqn = NvmeNqn::try_from(valid);
    assert_eq!(
        nqn,
        Ok(NvmeNqn::Org {
            date: "2014-08".to_string(),
            domain: "org.borg.long".to_string(),
            name: long_name
        })
    );
    assert_eq!(nqn.unwrap().to_string().len(), NVME_NQN_MAX_LEN);
    let invalid = format!(
        "{base_valid}{}",
        "a".repeat(NVME_NQN_MAX_LEN - base_valid.len() + 1)
    );
    let error = NvmeNqn::try_from(invalid);
    assert_eq!(error, Err(NvmeNqnParseError::TooLong));

    let base_valid = "nqn.2014-08.";
    let long_domain = "a".repeat(DOMAIN_LABEL_MAX_LEN);
    let valid = format!("{base_valid}{long_domain}:name");
    let nqn = NvmeNqn::try_from(valid);
    assert_eq!(
        nqn,
        Ok(NvmeNqn::Org {
            date: "2014-08".to_string(),
            domain: long_domain,
            name: "name".to_string()
        })
    );
    let long_domain = "a".repeat(DOMAIN_LABEL_MAX_LEN + 1);
    let valid = format!("{base_valid}{long_domain}:name");
    let error = NvmeNqn::try_from(valid);
    assert_eq!(error, Err(NvmeNqnParseError::DomainTooLong));
}
